/*
 * Author:
 * 		Davi Costa Clemente 		RA 083413
 * 		Fernando Lener de Arruda	RA 081391
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include "common.h"

char * addString(char * buffer, char *str) {
	int size = strlen(str);
	buffer = addInt(buffer, size);
	strcpy(buffer, str);
	return buffer + size;
}

char* readString(char * buffer, char **str) {
	int n;
	buffer = readInt(buffer, &n);

	*str = ALLOC(char, n+1);

	strcpy(*str, buffer);
	str[n] = (char) 0;

	return buffer + n;
}

char * addInt(char * buffer, int n) {
	int networkN = htonl(n);
	memcpy(buffer, (char*) &networkN, sizeof(int));
	return buffer + sizeof(int);
}

char * readInt(char *buffer, int *n) {
	*n = ntohl(*(int*) buffer);
	return buffer + 4;
}

int pseudoUDPConnect(char * address, unsigned short port, struct sockaddr_in* out_addr) {
	struct addrinfo *result;
	struct timeval timeout;
	//Usaremos um segundo de timeout
	timeout.tv_sec = 1;
	timeout.tv_usec = 0;

	int fd = socket(AF_INET, SOCK_DGRAM, 0);

	if (fd <= 0) {
		return fd;
	}

	if (getaddrinfo(address, 0, 0, &result) != 0) {
		return -1;
	}

	bzero(out_addr, sizeof(struct sockaddr_in));
	//Setamos o timeout
	setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, (sizeof(timeout)));

	out_addr->sin_family = AF_INET;
	out_addr->sin_port = htons(port);

	inet_pton(AF_INET, address, &out_addr->sin_addr);

	freeaddrinfo(result);

	return fd;
}

int createUDPSocket(unsigned short portno) {
	int sockfd, opt = 1;

	struct sockaddr_in serv_addr;

	sockfd = socket(AF_INET, SOCK_DGRAM, 0);

	if (sockfd < 0) {
		printf("Erro ao abrir o socket\n");
		return -1;
	}

	bzero((char *) &serv_addr, sizeof(serv_addr));

	serv_addr.sin_family = AF_INET;
	serv_addr.sin_addr.s_addr = INADDR_ANY;
	serv_addr.sin_port = htons(portno);

	setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

	if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) {
		printf("Erro no bind\n");
		printf("%s\n", strerror(errno));
		return -1;
	}

	return sockfd;
}

//TODO: testar essa função com as modificações
int safeSend(char * msg, int socket, struct sockaddr_in* in_addr) {
	unsigned structSize = sizeof(struct sockaddr);
	int writeResult = 0;
	int totalWriten = 0;
	int bytesLeft;

	readInt(msg, &bytesLeft);
	bytesLeft += 4;

	while (bytesLeft > 0) {
		writeResult = sendto(socket, (void*) msg + totalWriten, bytesLeft, 0,
				(struct sockaddr*)in_addr, structSize);
		if (-1 == writeResult) {
			return -1;
		}
		bytesLeft -= writeResult;
		totalWriten += writeResult;
	}

	return writeResult;
}

int safeReceive(char **msg, int fd, struct sockaddr_in* out_addr) {
	int n;
	unsigned structSize = sizeof(struct sockaddr);
	char buffer[BUFFER_SIZE];

	if((n=recvfrom(fd, buffer, BUFFER_SIZE, 0, (struct sockaddr*)out_addr, out_addr ? &structSize : 0))<0){
		*msg=0;
		return -1;
	}

	readInt(buffer, &n);

	*msg = ALLOC(char,n+1);
	memcpy(*msg,buffer+4,n);

	(*msg)[n] = '\0';

	return 0;
}

/*
 *	Formato da mensagens de request
 *	4 bytes = tamanho da mensagem
 *	4 bytes = tipo do cliente
 *	4 bytes = tipo da request
 *	opcional 4 bytes = tamanho do ISBN
 *	opcional variavel = ISBN
 *	opcional 4 bytes = valor a ser setado no estoque
 */

char * createBookRequest(RequestType rtype, ClientType client,
		char *optinalISBN, int optionalUpdateStockSize) {
	//The minimum size of request
	static char buffer[1000], *tempBuffer;

	//Inicializamos assim pq queremos espaço no começo
	//pra colocar o tamanho da mensagem
	tempBuffer = buffer + sizeof(int);

	tempBuffer = addInt(tempBuffer, client);
	tempBuffer = addInt(tempBuffer, rtype);

	switch (rtype) {
	case SINGLE_GetDescription:
	case SINGLE_GetFullInfo:
	case SINGLE_GetSampleCount:
	case SINGLE_UpdateStock:
		tempBuffer = addString(tempBuffer, optinalISBN);
		break;
	default:
		break;
	}

	if (rtype == SINGLE_UpdateStock) {
		tempBuffer = addInt(tempBuffer, optionalUpdateStockSize);
	}

	addInt(buffer, tempBuffer - buffer - 4);

	return buffer;
}

int receiveBookRequest(ClientType* out_ctype, RequestType* out_rtype,
		char **out_optionalISBN, int* out_optionalUpdateStockSize, int socket,
		struct sockaddr_in *addr) {
	char *msg, *temp;

	if (safeReceive(&msg, socket, addr) < 0)
		return -1;

	temp = msg;

	// Coloca em client o tipo de cliente, extraido de temp
	temp = readInt(temp, (int*) out_ctype);
	// Coloca em request o tipo de request, extraido de temp
	temp = readInt(temp, (int*) out_rtype);

	switch (*out_rtype) {
	case SINGLE_GetDescription:
	case SINGLE_GetFullInfo:
	case SINGLE_GetSampleCount:
	case SINGLE_UpdateStock:
		temp = readString(temp, out_optionalISBN);
		break;
	default:
		break;
	}

	if (*out_rtype == SINGLE_UpdateStock) {
		temp = readInt(temp, out_optionalUpdateStockSize);
	}

	free(msg);

	return 0;
}

void loadBooks(char *filename, Biblioteca* biblio) {
	FILE *arquivo = fopen(filename, "r");
	if (!arquivo) {
		printf("%s não encontrado\n", filename);
		exit(1);
	}
	fscanf(arquivo, "%d\n", &(biblio->numLivros));
	int i;
	biblio->livros = ALLOC(Livro,biblio->numLivros);

	for (i = 0; i < biblio->numLivros; i++) {
		fgets(biblio->livros[i].titulo, sizeof(biblio->livros[i].titulo),
				arquivo);
		fgets(biblio->livros[i].autor, sizeof(biblio->livros[i].autor),
				arquivo);
		fgets(biblio->livros[i].editora, sizeof(biblio->livros[i].editora),
				arquivo);
		fgets(biblio->livros[i].descricao, sizeof(biblio->livros[i].descricao),
				arquivo);
		fscanf(arquivo, "%d\n", &biblio->livros[i].anoPublicacao);
		fgets(biblio->livros[i].ISBN10, sizeof(biblio->livros[i].ISBN10),
				arquivo);
		fscanf(arquivo, "%d\n", &biblio->livros[i].exemplares);
	}

	fclose(arquivo);
}

void writeBooks(char *filename, Biblioteca * biblio) {
	FILE *file = fopen(filename, "w");
	int i;

	fprintf(file, "%d\n", biblio->numLivros);
	for (i = 0; i < biblio->numLivros; i++) {
		fprintf(file, "%s", biblio->livros[i].titulo);
		fprintf(file, "%s", biblio->livros[i].autor);
		fprintf(file, "%s", biblio->livros[i].editora);
		fprintf(file, "%s", biblio->livros[i].descricao);
		fprintf(file, "%d\n", biblio->livros[i].anoPublicacao);
		fprintf(file, "%s", biblio->livros[i].ISBN10);
		fprintf(file, "%d\n", biblio->livros[i].exemplares);
	}

	fclose(file);
}

int findBook(Biblioteca * b, char * isbn) {
	int i;

	for (i = 0; i < b->numLivros; i++) {
		if (strcmp(isbn, b->livros[i].ISBN10) == 0) {
			return i;
		}
	}
	return -1;
}

char * createBookRequestReply(ClientType ctype, RequestType rtype,
		char *optionalISBN, int optionalUpdateStockSize) {
	//Rezaremos para que seja o suficiente sempre ;)
	char *temp, *reply = ALLOC(char,BUFFER_SIZE);
	Livro * l = 0;
	int i, found = -1;
	Biblioteca biblioteca;
	struct timeval before, after;

	gettimeofday(&before, 0);

	*reply = 0;
	temp = reply + 8;

	if (rtype == SINGLE_UpdateStock && ctype == User) {
		temp += sprintf(temp,
				"Apenas a livraria pode atualizar o número de exemplares\n");
	} else if (rtype > CloseConnection || rtype < 0) {
		temp += sprintf(temp, "Operação inválida\n");
	} else {

		do {
			loadBooks(BIBLIO_FILENAME, &biblioteca);
			if (rtype == ALL_GetFullInfo || rtype == ALL_ISBNAndTitle) {
				for (i = 0; i < biblioteca.numLivros; i++) {
					l = biblioteca.livros + i;
					if (rtype == ALL_GetFullInfo) {
						temp +=
								sprintf(temp,
										"Título:%sAutor: %sEditora: %sDescrição: %s"
												"Ano de Publicação: %d\nISBN: %sExemplares: %d\n\n",
										l->titulo, l->autor, l->editora,
										l->descricao, l->anoPublicacao,
										l->ISBN10, l->exemplares);
					} else if (rtype == ALL_ISBNAndTitle) {
						temp += sprintf(temp, "Título: %sISBN: %s\n", l->titulo,
								l->ISBN10);
					}
				}
				break;

			}
			switch (rtype) {
			case SINGLE_GetDescription:
			case SINGLE_GetFullInfo:
			case SINGLE_UpdateStock:
			case SINGLE_GetSampleCount:
				found = findBook(&biblioteca, optionalISBN);
				break;
			default:
				break;
			}

			if (found < 0 && rtype != CloseConnection) {
				temp += sprintf(temp, "ISBN não encontrado\n");
				break;
			}

			l = biblioteca.livros + found;

			switch (rtype) {
			case SINGLE_GetDescription:
				temp += sprintf(temp, "Descrição: %s\n", l->descricao);
				break;
			case SINGLE_GetFullInfo:
				temp +=
						sprintf(temp,
								"Título: %sAutor: %sEditora: %sDescrição: %s"
										"Ano de Publicação: %d\nISBN: %sExemplares: %d\n\n",
								l->titulo, l->autor, l->editora, l->descricao,
								l->anoPublicacao, l->ISBN10, l->exemplares);

				break;
			case SINGLE_UpdateStock:
				l->exemplares = optionalUpdateStockSize;
				writeBooks(BIBLIO_FILENAME, &biblioteca);
				temp += sprintf(temp, "Exemplares atualizados\n");

				break;
			case SINGLE_GetSampleCount:
				temp += sprintf(temp, "Exemplares: %d\n", l->exemplares);

				break;
			case CloseConnection:
				temp += sprintf(temp, "ok\n");
				break;
			default:
				break;
			}

		} while (0);
	}

	gettimeofday(&after, 0);
	addInt(reply + 4,
			1000000 * (after.tv_sec - before.tv_sec)
					+ (after.tv_usec - before.tv_usec));

	addInt(reply, temp - (reply + 4) + 4);

	return reply;
}

